package org.alfresco.replication.rules;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ThreadPoolExecutor;

import org.alfresco.repo.action.ActionImpl;
import org.alfresco.repo.action.executer.ExecuteAllRulesActionExecuter;
import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy;
import org.alfresco.repo.policy.Behaviour;
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.transaction.TransactionListener;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.GUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 
 * Trigger the rules for the replication target spaces after the transfer is
 * finished. Relies on
 * {@link org.alfresco.replication.rules.ReplicationTargetSelector
 * ReplicationTargetSelector} for selecting target spaces using the transfer
 * report node.
 * 
 * @author Rui Fernandes
 * 
 */
public class TriggerReplicationTargetRules implements OnUpdatePropertiesPolicy,
        OnCreateNodePolicy
{

	static final String COMPLETE_TRANSFER_STATUS_VALUE = "COMPLETE";
	private static final String TRANSFER_MODEL_URI = "http://www.alfresco.org/model/transfer/1.0";
	static final QName TRANSFER_STATUS_PROPERTY = QName.createQName(
	        TRANSFER_MODEL_URI, "transferStatus");
	static final QName TRANSFER_RECORD_TYPE = QName.createQName(
	        TRANSFER_MODEL_URI, "transferRecord");
	private static final String KEY_TRANSFER_REPORT = TransferReportTransactionListener.class
	        .getName();
	private static final QName ON_UPDATE_PROPERTIES = QName.createQName(
	        NamespaceService.ALFRESCO_URI, "onUpdateProperties");
	private static final QName ON_CREATE_NODE = QName.createQName(
	        NamespaceService.ALFRESCO_URI, "onCreateNode");
	private static Log logger = LogFactory
	        .getLog(TriggerReplicationTargetRules.class);
	private ReplicationTargetSelector replicationTargetSelector;
	private ExecuteAllRulesActionExecuter executer;
	private PolicyComponent policyComponent;
	private TransactionService transactionService;
	private ThreadPoolExecutor threadExecuter;
	private TransactionListener transactionListener;
	private BehaviourFilter policyFilter;
	protected AuthenticationComponent authenticationComponent;
	protected NodeService nodeService;

	public void setNodeService(NodeService nodeService)
	{
		this.nodeService = nodeService;
	}

	public void setAuthenticationComponent(
	        AuthenticationComponent authenticationComponent)
	{
		this.authenticationComponent = authenticationComponent;
	}

	public TriggerReplicationTargetRules()
	{
		this.transactionListener = new TransferReportTransactionListener();
	}

	public void setPolicyFilter(BehaviourFilter policyFilter)
	{
		this.policyFilter = policyFilter;
	}

	public void setTransactionService(TransactionService transactionService)
	{
		this.transactionService = transactionService;
	}

	public void setThreadExecuter(ThreadPoolExecutor threadExecuter)
	{
		this.threadExecuter = threadExecuter;
	}

	/** Id used to identify the test action created */
	private final static String ID = GUID.generate();

	public void setPolicyComponent(PolicyComponent policyComponent)
	{
		this.policyComponent = policyComponent;
	}

	public void setExecuteAllRulesActionExecuter(
	        ExecuteAllRulesActionExecuter executer)
	{
		this.executer = executer;
	}

	public void setReplicationTargetSelector(
	        ReplicationTargetSelector replicationTargetSelector)
	{
		this.replicationTargetSelector = replicationTargetSelector;
	}

	public void init()
	{
		Behaviour onUpdatePropertiesBehaviour = new JavaBehaviour(this,
		        "onUpdateProperties", NotificationFrequency.TRANSACTION_COMMIT);
		this.policyComponent.bindClassBehaviour(ON_UPDATE_PROPERTIES,
		        TRANSFER_RECORD_TYPE, onUpdatePropertiesBehaviour);

		Behaviour onCreateNodeBehaviour = new JavaBehaviour(this,
		        "onCreateNode", NotificationFrequency.TRANSACTION_COMMIT);
		this.policyComponent.bindClassBehaviour(ON_CREATE_NODE,
		        TRANSFER_RECORD_TYPE, onCreateNodeBehaviour);
	}

	/**
	 * Checks if the properties settings include transferStatus COMPLETE. So
	 * that before did not existed or had a different value.
	 */
	@Override
	public void onUpdateProperties(NodeRef nodeRef,
	        Map<QName, Serializable> fromProperties,
	        Map<QName, Serializable> toProperties)
	{
		if (!isComplete(fromProperties) && isComplete(toProperties))
		{
			bindListener(nodeRef);
		}
	}

	private void bindListener(NodeRef nodeRef)
	{
		log(String.format("Report (ID %s) binding listener",
		        nodeRef.getId()));
		AlfrescoTransactionSupport.bindListener(transactionListener);
		@SuppressWarnings("unchecked")
		Set<NodeRef> nodes = (Set<NodeRef>) AlfrescoTransactionSupport
		        .getResource(KEY_TRANSFER_REPORT);
		if (nodes == null)
		{
			nodes = new HashSet<NodeRef>(5);
			AlfrescoTransactionSupport.bindResource(KEY_TRANSFER_REPORT, nodes);
		}
		nodes.add(nodeRef);
	}

	private void triggerRules(NodeRef nodeRef)
	{
		log("Executing the target rules. Replication report is COMPLETE.");
		executeRulesReportTargetSpaces(nodeRef);
		log("Target rules executed after replication report completion.");
	}

	private void executeRulesReportTargetSpaces(NodeRef reportNode)
	{
		for (NodeRef targetSpace : replicationTargetSelector
		        .getTargetSpaces(reportNode))
		{
			log(String.format("Executing the target space %s rules.",
			        targetSpace.getId()));
			executeRulesTargetSpace(targetSpace);
			log(String.format("Target space %s rules executed.",
			        targetSpace.getId()));
		}

	}

	private void executeRulesTargetSpace(NodeRef targetSpace)
	{

		ActionImpl action = new ActionImpl(null, ID,
		        ExecuteAllRulesActionExecuter.NAME, null);
		action.setParameterValue(
		        ExecuteAllRulesActionExecuter.PARAM_RUN_ALL_RULES_ON_CHILDREN,
		        Boolean.TRUE);
		action.setParameterValue(
		        ExecuteAllRulesActionExecuter.PARAM_EXECUTE_INHERITED_RULES,
		        Boolean.TRUE);
		this.executer.execute(action, targetSpace);

	}

	private boolean isComplete(Map<QName, Serializable> props)
	{
		return props != null
		        && props.containsKey(TRANSFER_STATUS_PROPERTY)
		        && props.get(TRANSFER_STATUS_PROPERTY).equals(
		                COMPLETE_TRANSFER_STATUS_VALUE);
	}

	@Override
	public void onCreateNode(ChildAssociationRef child)
	{
		NodeRef node = child.getChildRef();
		if (isComplete(nodeService.getProperties(node)))
			bindListener(node);

	}

	private class TransferReportTransactionListener extends
	        TransactionListenerAdapter
	{

		@Override
		public void afterCommit()
		{
			// Get all the nodes that need their read counts incremented
			@SuppressWarnings("unchecked")
			Set<NodeRef> reportNodes = (Set<NodeRef>) AlfrescoTransactionSupport
			        .getResource(KEY_TRANSFER_REPORT);
			if (reportNodes != null)
			{
				for (NodeRef nodeRef : reportNodes)
				{
					Runnable runnable = new TranferReportTrigger(nodeRef);
					threadExecuter.execute(runnable);
				}
			}
		}
	}

	private class TranferReportTrigger implements Runnable
	{

		private NodeRef nodeRef;

		public TranferReportTrigger(NodeRef nodeRef)
		{
			this.nodeRef = nodeRef;
		}

		@Override
		public void run()
		{

			RetryingTransactionHelper txnHelper = transactionService
			        .getRetryingTransactionHelper();
			RetryingTransactionCallback<Integer> callback = new RetryingTransactionCallback<Integer>()
			{
				public Integer execute() throws Throwable
				{
					try
					{
						// Ensure that the policy doesn't refire for this node
						// on this thread
						// This won't prevent background processes from
						// refiring, though
						policyFilter.disableBehaviour(nodeRef,
						        ON_UPDATE_PROPERTIES);
						policyFilter.disableBehaviour(nodeRef, ON_CREATE_NODE);
						triggerRules(nodeRef);
						return 0;
					} finally
					{
						policyFilter.enableBehaviour(ON_UPDATE_PROPERTIES);
						policyFilter.enableBehaviour(ON_CREATE_NODE);
					}
				}
			};
			String user = authenticationComponent.getCurrentUserName();
			try
			{
				authenticationComponent.setSystemUserAsCurrentUser();
				txnHelper.doInTransaction(callback, false, true);
			} catch (InvalidNodeRefException e)
			{
				logger.error(e);

				log("Unable to trigger rules on target for missing report node: "
				        + nodeRef);

			} catch (Throwable e)
			{
				logger.error(e);
				log("Failed to trigger rules on target for report node: "
				        + nodeRef);
			} finally
			{
				if (user != null)
				{
					authenticationComponent.setCurrentUser(user);
				} else
				{
					authenticationComponent.clearCurrentSecurityContext();
				}
			}
		}

	}
	
	private static void log(String msg){
		if(logger.isDebugEnabled()){
			logger.debug(msg);
		}
	}
}
